package ch.ethz.fcl.metrobuzz.data.transit;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import ch.ethz.fcl.metrobuzz.data.MBData;
import ch.ethz.fcl.metrobuzz.data.accessibility.cost.MBEdgeCost;
import ch.ethz.fcl.metrobuzz.data.scene.basemap.Edge;
import ch.ethz.fcl.metrobuzz.data.transit.bus.RawBusSchedule;
import ch.ethz.fcl.metrobuzz.tools.MBTools;

/**
 * TransitRoute is consist of Mode, RouteProfile(linked stops), Route(links in
 * the network file) and Departures
 * 
 * @author ZengWei
 * 
 */
public class TransitRoute {
	private MBData data;
	private TransitLine line;
	private String mode;
	private String id;

	private List<Stop> stopList;
	private Map<Stop, Integer> stopCountMap;
	// stop id to the index map,
	// note the route may pass through one stop two times (e.g. 199 starts and
	// ends at Boon Lay)
	// in such case, the second time stop id will be '_2'
	// and if more than 2, will be '-3' (maximum 3)
	private Map<String, Integer> stopIndexMap;

	// stop distance between start and other stops
	private List<Float> stopsDist;
	private float routeDist = -1;

	private List<Edge> edgeList;
	private Map<Edge, Integer> edgeCountMap;
	private Map<String, Integer> edgeIndexMap;

	// Record buses schedules on this transit line
	private Map<Integer, RawBusSchedule> busMap;

	// time cost between start and other stops
	private List<Float>[] stopsTime;

	private Map<ODStop, List<Edge>> stopInterEdgesMap;

	@SuppressWarnings("unchecked")
	public TransitRoute(MBData data, TransitLine line, String id) {
		this.data = data;
		this.line = line;
		this.id = id;

		stopList = new ArrayList<Stop>();
		stopCountMap = new HashMap<Stop, Integer>();
		stopIndexMap = new LinkedHashMap<String, Integer>();

		edgeList = new ArrayList<Edge>();
		edgeCountMap = new HashMap<Edge, Integer>();
		edgeIndexMap = new LinkedHashMap<String, Integer>();

		stopsDist = new ArrayList<Float>();

		busMap = new HashMap<Integer, RawBusSchedule>();

		stopsTime = (List<Float>[]) new List<?>[MBData.ONE_DAY_TIME];

		stopInterEdgesMap = new HashMap<ODStop, List<Edge>>();
	}

	public void calStopDist() {
		if (stopList.size() == 0)
			return;

		// calculate distance between two consecutive stops in the route
		if (mode.equals("bus")) {
			stopsDist.add(0F);
			for (int i = 1; i < stopList.size(); i++) {
				Stop preStop = stopList.get(i - 1);
				Stop nextStop = stopList.get(i);
				float dist = 0;

				List<Edge> interEdges = getInterEdges(preStop, nextStop);

				for (Edge e : interEdges) {
					dist += e.distance();
				}

				stopsDist.add(stopsDist.get(i - 1) + dist);
			}
		}

	}

	public void calStopTime() {
		if (stopList.size() == 0)
			return;

		// calculate distance between two consecutive stops in the route
		if (mode.equals("bus")) {
			for (int t = 0; t < MBData.ONE_DAY_TIME; t++) {
				stopsTime[t] = new ArrayList<Float>();

				stopsTime[t].add(0F);
				for (int i = 1; i < stopList.size(); i++) {
					Stop preStop = stopList.get(i - 1);
					Stop nextStop = stopList.get(i);
					float pathCost = 0;

					List<Edge> interEdges = getInterEdges(preStop, nextStop);

					for (Edge e : interEdges) {
						MBEdgeCost roadCost = data.getCost().getEdgeCost(e);
						if (roadCost == null) {
							float speed = 5; // default bus speed 5 m/s

							pathCost += e.distance() / speed;
						} else
							pathCost += roadCost.getCost(t);
					}

					stopsTime[t].add(stopsTime[t].get(i - 1) + pathCost);
				}
			}
		}

		// if (id.equals("30_weekday_2")) {
		// printStops();
		// }
	}

	public void passengerTap(String input, int busReg, Stop borStop,
			float borTime, Stop alightStop, float alightTime) {
		calEdgeEfficiency(busReg, borStop, borTime, alightStop, alightTime);
	}

	private void calEdgeEfficiency(int busReg, Stop borStop, float borTime,
			Stop alightStop, float alightTime) {
		int[] indices = MBTools.getIndices(stopCountMap, stopIndexMap, borStop,
				borStop.getId(), alightStop, alightStop.getId());
		if (indices[0] == -1 || indices[1] == -1) {
			return;
		}

		List<Edge> interEdges = getInterEdges(borStop, alightStop);
		if (interEdges == null)
			return;

		float dist = stopsDist.get(indices[1]) - stopsDist.get(indices[0]);

		float speed = dist / (alightTime - borTime);

		float passTime = 0;
		for (Edge e : interEdges) {
			data.getCost().addRoadCost(e, borTime + passTime,
					e.distance() / speed);
			passTime += e.distance() / speed;
		}
	}

	public Map<Integer, RawBusSchedule> getBusSchedule() {
		return busMap;
	}

	public TransitLine getLine() {
		return line;
	}

	public void setMode(String mode) {
		this.mode = mode;
	}

	public void addStop(Stop stop) {
		Integer count = stopCountMap.get(stop);
		String id = stop.getId();
		if (count == null) {
			stopIndexMap.put(id, stopList.size());
			stopCountMap.put(stop, 1);
		} else {
			id = id + MBTools.SEPERATOR + count;
			stopIndexMap.put(id, stopList.size());
			stopCountMap.put(stop, count + 1);
		}

		stopList.add(stop);
		stop.addPassingRoute(this);
	}

	public void addEdge(Edge edge) {
		String id = edge.getId();
		Integer count = edgeCountMap.get(edge);

		if (count == null) {
			edgeCountMap.put(edge, 1);
			edgeIndexMap.put(id, edgeList.size());
		} else {
			id = id + MBTools.SEPERATOR + count;
			edgeIndexMap.put(id, edgeList.size());
			edgeCountMap.put(edge, count + 1);
		}

		edgeList.add(edge);
		line.getTransitNetwork().addTransitEdge(edge);
	}

	public String getMode() {
		return mode;
	}

	public List<Edge> getEdges() {
		return edgeList;
	}

	public String getId() {
		return id;
	}

	public List<Stop> getStops() {
		return stopList;
	}

	/**
	 * return index of the stop in the route
	 * 
	 * @param stop
	 * @return -1 indicates stop not found in this route
	 */
	public int getStopIndex(Stop stop) {
		Integer index = stopIndexMap.get(stop.getId());

		return index == null ? -1 : index;
	}

	public float getRouteDist() {
		if (routeDist == -1) {
			routeDist = 0;
			for (int i = 0; i < stopsDist.size(); i++)
				routeDist += stopsDist.get(i);
		}

		return routeDist;
	}

	public float getStopDist(Stop begin, Stop end) {
		Integer beginIndex = stopIndexMap.get(begin.getId()), endIndex = stopIndexMap
				.get(end.getId());

		if (beginIndex > endIndex)
			endIndex = stopIndexMap.get(end.getId() + "_2");

		if (endIndex == null)
			return -1;
		else if (beginIndex == endIndex)
			return 0;
		else {
			float dist = 0;
			for (int i = beginIndex; i < endIndex; i++)
				dist += stopsDist.get(i);
			return dist;
		}
	}

	/**
	 * @param current
	 *            stop
	 * @return the next stops
	 */
	public List<Stop> getNextStops(Stop current) {
		List<Stop> nextStops = new ArrayList<Stop>();

		int index = getStopIndex(current);
		if (index == -1 || index == stopList.size() - 1)
			return nextStops;

		for (int j = index + 1; j < stopList.size(); j++)
			nextStops.add(stopList.get(j));

		return nextStops;
	}

	/**
	 * 
	 * @param timeIndex
	 * @param start
	 * @param end
	 * @return
	 */

	public float getCost(int timeIndex, Stop start, Stop end) {
		int[] indices = MBTools.getIndices(stopCountMap, stopIndexMap, start,
				start.getId(), end, end.getId());

		if (indices[0] == -1 || indices[1] == -1)
			return -1;

		return stopsTime[timeIndex].get(indices[1])
				- stopsTime[timeIndex].get(indices[0]);
	}

	public float getCost(int timeIndex, int startIndex, int endIndex) {
		return stopsTime[timeIndex].get(endIndex)
				- stopsTime[timeIndex].get(startIndex);
	}

	/**
	 * @param beginStop
	 * @param endStop
	 * @return inter stops(including begin and end) between begin and end stops
	 */
	public List<Stop> getInterStops(Stop beginStop, Stop endStop) {
		int[] indices = MBTools.getIndices(stopCountMap, stopIndexMap,
				beginStop, beginStop.getId(), endStop, endStop.getId());

		if (indices[0] == -1 || indices[1] == -1 || indices[0] >= indices[1])
			return null;
		List<Stop> interstops = new ArrayList<Stop>();
		for (int i = indices[0]; i <= indices[1]; i++)
			interstops.add(stopList.get(i));

		return interstops;
		// }
	}

	/**
	 * 
	 * @param startStop
	 * @param endStop
	 * @return list of edges between the start and end stops
	 */
	public List<Edge> getInterEdges(Stop startStop, Stop endStop) {
		ODStop odStop = new ODStop(startStop, endStop);
		List<Edge> interEdges = stopInterEdgesMap.get(odStop);

		if (interEdges != null)
			return interEdges;
		else {
			Edge startEdge = startStop.getRefEdge();
			Edge endEdge = endStop.getRefEdge();
			int[] indices = MBTools.getIndices(edgeCountMap, edgeIndexMap,
					startEdge, startEdge.getId(), endEdge, endEdge.getId());

			if (indices[0] == -1 || indices[1] == -1
					|| indices[0] >= indices[1]) {
				stopInterEdgesMap.put(odStop, null);
				return null;
			}

			interEdges = new ArrayList<Edge>();
			for (int i = indices[0]; i <= indices[1]; i++)
				interEdges.add(edgeList.get(i));

			stopInterEdgesMap.put(odStop, interEdges);
			return interEdges;
		}
	}

	public void printStops() {
		List<String> stopIds = new ArrayList<String>(stopIndexMap.keySet());
		System.out.print(id + " stops: ");
		for (int i = 0; i < stopList.size(); i++)
			System.out.println((i + 1) + " " + stopList.get(i).getId() + " "
					+ stopIds.get(i) + " dist " + stopsDist.get(i) + " time "
					+ stopsTime[MBData.startTime].get(i));
		System.out.println();
	}

	public void printEdges() {
		List<String> edgeIds = new ArrayList<String>(edgeIndexMap.keySet());
		System.out.print("Edges: ");
		for (int i = 0; i < edgeList.size(); i++)
			System.out.println(edgeList.get(i).getId() + " " + edgeIds.get(i));
		System.out.println();
	}

	class ODStop {
		Stop origin;
		Stop end;

		public ODStop(Stop origin, Stop end) {
			this.origin = origin;
			this.end = end;
		}

		@Override
		public boolean equals(Object o) {
			if (!(o instanceof ODStop))
				return false;

			ODStop os = (ODStop) o;
			if (os.origin.equals(origin) && os.end.equals(end))
				return true;

			return false;
		}

		@Override
		public int hashCode() {
			String id = origin.getId() + MBTools.SEPERATOR + end.getId();
			return id.hashCode();
		}
	}
}
